﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpressionMapper
{
    using MappingExpressionGenerator = Func<Expression, Type, Expression, ParameterExpression, Expression>;

    public static partial class Generators
    {

        /// <summary>
        /// Returns generator for complex type mappings.
        /// </summary>
        public static MappingExpressionGenerator ComplexToComplex(MappingExpressionGenerator parent,
                                                                  IMemberExtractor memberExtractor)
        {
            return (fromExpr, to, customMapping, instanceDictionary) =>
                       {
                           if (IsComplexType(fromExpr.Type) && IsComplexType(to))
                           {
                               return ValidateCustomMapping(customMapping)
                                      ??
                                      GenerateConplexTypeExpression(parent, memberExtractor, fromExpr, to,
                                                                          customMapping, instanceDictionary);
                           }

                           return null;
                       };
        }

        private static Expression ValidateCustomMapping(Expression customMapping)
        {
            if (customMapping != null &&
                (customMapping.NodeType != ExpressionType.MemberInit
                 || (customMapping as MemberInitExpression).Bindings.Count == 0))
            {
                return customMapping;
            }

            return null;
        }

        private static Expression GenerateConplexTypeExpression(MappingExpressionGenerator parent,
                                                                      IMemberExtractor memberExtractor,
                                                                      Expression fromExpr, Type to,
                                                                      Expression customMapping,
                                                                      ParameterExpression instanceDictionary)
        {
            if (IsAssignableNullable(fromExpr.Type, to))
                throw new InvalidOperationException();

            Debug.WriteLine(string.Format("ComplexToComplex ({0}): {1} to {2}", fromExpr.NodeType, fromExpr.Type.Name, to.Name));

            NewExpression configNewExpr = null;
            IEnumerable<MemberAssignment> configBindings = Enumerable.Empty<MemberAssignment>();
            var customInitExpression = customMapping as MemberInitExpression;
            if (customInitExpression != null)
            {
                configNewExpr = customInitExpression.NewExpression;
                configBindings = customInitExpression.Bindings.OfType<MemberAssignment>();
            }

            Expression sourceInstance = fromExpr;
            Expression castedParam = null;
            if (IsObject(fromExpr.Type))
            {
                 sourceInstance = Expression.Variable(to, "casted");
                 castedParam = Expression.Assign(sourceInstance, Expression.Convert(fromExpr, to));
            }

            IList<MappingMember> membersTo = memberExtractor.GetToMembers(to).ToList();

            IList<MappingMember> membersFrom = memberExtractor.GetFromMembers(sourceInstance.Type, membersTo).ToList();

            IList<MemberAssignment> conventionalBindings = (from mfrom in membersFrom
                                                            join mto in membersTo on mfrom.Name equals mto.Name
                                                            let memberExpr = GetMemberExpression((x) => parent(x, mto.Type, null, instanceDictionary),
                                                                                                 mfrom.GetMemberAccessExpression(sourceInstance),
                                                                                                 mfrom.Type, mto.Type, mfrom, instanceDictionary)
                                                            where memberExpr != null
                                                            select Expression.Bind(mto.Member, memberExpr)
                                                           ).ToList();


            //HACK : In case of no member "from" for custom mapping
            //used binding expression as sourceInstance parameter for recursive call
            //because in this case sourceInstance used only for type matching
            List<MemberAssignment> customBindings = (from b in configBindings
                                                     join mto in membersTo on b.Member equals mto.Member
                                                     let mfrom = membersFrom.FirstOrDefault(m => mto.Name == m.Name)
                                                     let expr =
                                                         (mfrom != null)
                                                             ? mfrom.GetMemberAccessExpression(sourceInstance)
                                                             : b.Expression
                                                     /*hack*/
                                                     let memberExpr =
                                                         GetMemberExpression((x) => parent(x, mto.Type, b.Expression, instanceDictionary),
                                                                             expr, mfrom == null ? b.Expression.Type : mfrom.Type,
                                                                             mto.Type, mfrom, instanceDictionary)
                                                     //where memberExpr != null 
                                                     select Expression.Bind(mto.Member, memberExpr)
                                                    ).ToList();

            var customBindingMembers = new HashSet<MemberInfo>(customBindings.Select(b => b.Member));
            IEnumerable<MemberAssignment> allBindings = customBindings
                .Concat(conventionalBindings
                            .Where(b => !customBindingMembers.Contains(b.Member)));

            

            var clone = Expression.Variable(to, "clone");
            var initClone = Expression.Assign(clone, Expression.MemberInit(configNewExpr ?? Expression.New(to)));
            IList<Expression> memberinits = new List<Expression>();
            if (castedParam != null)
                memberinits.Add(castedParam);
            memberinits.Add(initClone);
            memberinits.Add(Expression.Call(instanceDictionary, "Add", null, fromExpr, clone));
            
            // object clone = new ()
            foreach (var memberAssignment in allBindings)
            {
                // initialize each available field and property
                memberinits.Add(Expression.Assign(Expression.MakeMemberAccess(clone, memberAssignment.Member), memberAssignment.Expression));
            }
            // return clone
            memberinits.Add(clone);
            BlockExpression initBlock;
            if(castedParam != null)
                initBlock = Expression.Block(new[] { clone, sourceInstance as ParameterExpression }, memberinits);
            else
                initBlock = Expression.Block(new[] { clone }, memberinits);

            
            ParameterExpression copy = Expression.Variable(typeof(object), "copy");
            Expression mainBlock = Expression.Block(new[] { copy },
                Expression.Condition(
                    Expression.Call(instanceDictionary, "TryGetValue", null
                                    , Expression.Convert(fromExpr, typeof(object)), copy),
                        Expression.Convert(copy, to),
                        initBlock
                ));

            Expression ifNullExpr = Expression.Condition(
                  Expression.Equal(Expression.Constant(null, fromExpr.Type), fromExpr),
                     Expression.Constant(null, to),
                         mainBlock
                       );
            return ifNullExpr;
        }

      

        private static Expression GetMemberExpression(Func<Expression, Expression> getMemberAccessExpr,
                                                      Expression memberExpression, Type fromType, Type toType, MappingMember member, ParameterExpression dictionary)
        {

            if (member != null)
            {
                Debug.WriteLine(string.Format("GetMember {0}.{1} of {2}",
                                      member.Member.DeclaringType != null
                                          ? member.Member.DeclaringType.Name
                                          : string.Empty
                                      , member.Name, member.Type.Name));
            }

            if (IsComplexType(fromType) && !IsNullable(fromType) && !IsList(fromType) && !fromType.IsArray)
            {
                return GetMappingExpression(memberExpression, fromType, toType, dictionary);
            }
            return getMemberAccessExpr(memberExpression);
        }

        private static Expression GetMappingExpression(Expression memberExpression, Type fromType, Type toType, ParameterExpression dictionary)
        {
            MethodInfo convertAllMethod = typeof(Mapper)
                .GetMethod("Map", BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(fromType, toType);

            return Expression.Call(convertAllMethod, memberExpression, dictionary);
        }

        private static bool IsComplexType(Type t)
        {
            return (t.IsClass && t != typeof(string) && !t.IsAbstract && CanCreate(t))
                   || (t.IsValueType && !IsPrimitive(t));
        }
        private static bool CanCreate(Type t)
        {
            return t.GetConstructors().Length > 0 && !IsEnumerable(t);
        }
    }
}